"""
скрипт упралвения пакетами Чио.

автор: Fisakura Nirvalen
версия: 1.7 rf-v5
репозиторий: https://notabug.org/pentergust/cpm
"""

import sys
import os
import toml
import shutil
import aiohttp
import asyncio
import hashlib
import time
import fnmatch
from icecream import ic


"""
    package_base
    ----------+-------------------------------------------------------
    name      | имя пакета
    desc      | краткое описание пакета (заголовок)
    full_desc | полное описание пакета (прим. перечень функций)
    ver       | аерсия пакета (строка)
    build     | номер сборки пакета (число)
    type      | группа пакета (ядро, библиотека, плагин)
    authors   | авторы пакета (список)
    require   | список PID зависимостей
    files     | список путей файлов пакета
    
    * после преобразованния появиляются некоторые новые пункты
    used      | каким пакетам требуется данный пакет
    files     | преобразаутеся словарь {путь к файлу: md5 хеш}
    payh      | путь к папке пакета
    ----------+---+---------------------------------------------------
    cpm_data_path | путь к папке данных cpm
    indexfile     | название индексного файла по умолчанию
    --------------+---------------------------------------------------
"""

cpm_data_path = "cpm_data/"
indexfile = "index"
package_base = {
    "name": "package", "desc": "no desc", "full_desc": "no full desc",
    "ver": "1.0", "build": 1, "type": "package",
    "authors": [], "require": [], "files": {}, "path": ""
}

# разнообразные вспомогательные функции
# =====================================

def group(text):
    """Отображает сообщение о группе действий.
    :param text: текст сообщения"""

    print(f'\033[34m:: \033[97m{text}\033[0m')


def do_you_want_to_continue():
    """Спрашивает у пользователя желает ли он продолжить операцию.

    :returns: True/False"""

    i = input('do you want to continue (Y/N): ') or ''
    if i.lower()[0] in ['y', 'д']:
        return True

    return False


def check_params(data, model):
    """Дополняет отцуцтвующие параметры словаря по образцу
    :param data: словарь, который необходимо проверить
    :param model: словарь-образец, по которому делается проверка

    :return: словарь с проверенными параметрами"""

    for k, v in model.items():
        if k not in data or data[k] is None:
            data[k] = v

    return data


def select(selectors, deep=0):
    """меню выбора значеия из списка

    :param selectrs: список значений для выбора
    :optional param deep: глубина выбора

    :return: обно значение из списка"""

    if deep is None:
        deep = 0

    # отображаем список элементов
    for i, x in enumerate(selectors):
        print(f'{"-"*deep}{i+1}. \033[36m{x}\033[0m')

    # выбираем значение из списка
    while True:
        i = input('(0-9, a-z) select one> ')

        # если указана
        if i.isdigit():
            return selectors[max(1, min(int(i), len(selectors)))-1]

        else:
            res = []
            for x in selectors:
                if i in x.lower():
                    res.append(x)

            if not res:
                print(f'\033[91m{i} \033[31mnot found\033[0m')
                continue

            if len(res) == 1:
                return res[0]
            else:
                print(f'* select {len(res)} varients...')
                return select(res, deep+1)


def log(text, log_type="i"):
    """Выводит сообщения в терминал
    :param text: текст сообщения
    :optional param log_type: тип сообщения ->
        i | информация
        c | завершение дейстия
        w | предупреждение
        e | ошибка
    """

    log_types = {
        'i': "",
        'c': "\033[96m*\033[36m",
        'w': "\033[33mWARN\033[0m",
        'e': "\033[91mERR\033[0m",
        'a': "\033[35m>\033[90m"
    }

    if log_type in log_types:
        group = log_types[log_type]

    elif log_type:
        group = f"\033[93m[{log_type}]\033[0m"

    print(f" {group} {text}\033[0m")


def get_file_hash(path):
    """Получает md5 хеш файла по его пути."""
    if not os.path.exists(path):
        return ""

    with open(path, 'rb') as f:
        return hashlib.md5(f.read()).hexdigest()


def dir_iterator(places):
    """Применяется при переборе директорий, вместо рекурсии.
    Возвращает по одной директории из списка, пропуская скрытые"""

    for place in places:
        last_folder = place.rsplit('/', 1)[-1]

        if len(places) > 1 and last_folder[0] in ['.', '_']:
            continue

        yield place


def get_package_files(path):
    """получает словарь файлов пакетов, типа {путь: хеш}
    :param path: начальный путь для поиска пакетов"""
    res = {}
    places = [path]

    for place in dir_iterator(places):
        if len(places) > 1 and 'packages.toml' in os.listdir(place):
            continue

        for x in os.listdir(place):
            p = os.path.join(place, x)

            if os.path.isdir(p):
                places.append(p)

            else:
                res[p] = get_file_hash(p)

    return res


async def load_file_from_url(session, url):
    """Загружает файл по ссылке
    :param session: aiohttp клиентская сессия
    :param url: ссылка для загрузки файла"""

    try:
        async with session.get(url) as r:
            if r.status != 200:
                log(f'{r.status}: {url}', 'e')
                return {}

            log(f'OK: {url}')
            return await r.text()
    except Exception as e:
        log(f'{url}: {e}', 'e')
        return {}


async def load_files(session, files):
    """Загружает файлы из словаря {path:url}

    :param session: aiohttp сессия
    :paeam files: словарь {path: url} для загрузки файлов"""

    for k, v in files.items():
        file = await load_file_from_url(session, v)
        p = k.rsplit('/', 1)[0]

        if not os.path.exists(p):
            os.makedirs(p)

        if file:
            with open(k, 'w') as f:
                f.write(file)


def print_dict(data):
    """выводит содержимое словаря
    :param data: словарь, который удно отобразить"""
    s = max(map(len, data.keys()))

    for k, v in data.items():
        if isinstance(v, list):
            v = ', '.join(map(str, v))

        if isinstance(v, dict):
            res = ""
            for nk, nv in v.items():
                res += f'\n{nk}: {nv}'
            v = res

        if isinstance(v, str):
            v = v.replace('\n', '\n'+' '*s+' | ')

        print(f'\033[36m{k}\033[0m{" "*(s-len(k))} | {v}')


# загрузка и сохранение toml файла
# ================================

def load_file(path):
    if not path.count('.'):
        path += '.toml'

    if not os.path.exists(path):
        return {}

    with open(path) as f:
        try:
            return toml.loads(f.read())
        except Exception as e:
            log(e, 'e')
            return {}


def save_file(path, data):
    if not path.count('.'):
        path += '.toml'

    p = path.rsplit('/', 1)
    if not os.path.exists(p[0]) and len(p) == 2:
        os.makedirs(p[0])

    log(f'save: {path}', 'a')
    with open(path, 'w') as f:
        f.write(toml.dumps(data))
        return data


class Config:
    """Небольшой класс, управляющий файлом настроек CPM.
    :param config_path: путь к файлу с настройками"""

    def __init__(self, cpm_path=cpm_data_path):
        super(Config, self).__init__()
        self.cpm_path = cpm_path

        self.repositories_path = os.path.join(
            self.cpm_path, 'repositories.toml')
        self.package_base_path = os.path.join(
            self.cpm_path, 'package_base.toml')

        """
            packages_model.toml - осписание стандартных параметров пакета
            repositories.toml   - описание репозиториев cpm

            описание реопзториев в файле: {cpm_path}/repositories.toml
            ==========================================================
            repository_name: {url:str, updated:int}

                url     | ссылка для загрзуки файлов из рнпозитория {URL}/{PATH}
                updated | unixtime последней загрузки из репозитория
        """
        self.repositories = {
            "chiohub": {
                "url": "https://notabug.org/pentergust/ChioHub/raw/master/",
                "updated": 0},
            "cpm": {
                "url": "https://notabug.org/pentergust/cpm/raw/master",
                "updated": 0}}

    def read(self):
        """Читает файл настроек из self.config_path.
        Если файл лтцуцтвует, создаёт его"""

        config = {}

        if not os.path.exists(self.repositories_path):
            config['repositories'] = self.repositories
            save_file(self.repositories_path, self.repositories)
        else:
            config['repositories'] = load_file(self.repositories_path)

        if not os.path.exists(self.package_base_path):
            config['package_model'] = package_base
            save_file(self.package_base_path, package_base)
        else:
            config['package_model'] = load_file(self.package_base_path)

        return config


# вспомогательный класс для создания консольного приложения
# =========================================================

class SearchEngine:
    """docstring for SearchPackages"""

    def __init__(self, index):
        super(SearchEngine, self).__init__()
        self.index = index

        self.groups = {}
        self.response = {}

    def add_in_group(self, pid, name, key, value):
        """Сравнрвает параметры пакета с поисковым запросом.
        Добавляет резульат в self.groups если есть совпадение.

        :param data: словарь с текущими результатами поиска
        :param pid: package ID для которого ведётся поиск
        :param name: имя группы поиска
        :param key: строка, для которой применяется поиск
        :param value: ввод от пользователя
        """

        if not isinstance(key, str):
            key = str(key)

        if value.lower() in key.lower():
            if not pid in self.groups:
                self.groups[pid] = [name]
            else:
                self.groups[pid].append(name)

    def gen_response(self):
        """Группирует значения из self.groups в self.response"""

        for k, v in self.groups.items():
            g = ", ".join(v)

            if g not in self.response:
                self.response[g] = [k]
            else:
                self.response[g].append(k)

    def get_groups(self, args):
        """Собирает данные о пакетах их self.index в self.groups.

        :param args: строка поиска по пакетам в индексе"""

        for k, v in self.index.data.items():
            self.add_in_group(pid=k, name='pid', key=k, value=args)

            for n, t in v.items():
                if isinstance(t, list):
                    for x in t:
                        self.add_in_group(pid=k, name=n, key=x, value=args)
                    continue

                self.add_in_group(pid=k, name=n, key=t, value=args)

    def search(self, args):
        """ищет пакеты по их параметрам и возвращает PID

        :param args: строка для поиска"""

        self.get_groups(args)
        self.gen_response()

        for k, v in sorted(self.response.items(), key=lambda x: len(x[1])):
            print(f'\n{k}')
            self.index.packages_list(v)


class Context:
    def __init__(self, args=[], keys={}):
        self.keys = keys
        self.args = args
        self.largs = ', '.join(map(str, args))
        self.sargs = ' '.join(map(str, args))

    def __iter__(self):
        for a in self.args:
            yield a

    def get(self, key, value=None):
        return self.keys.get(key, value)

    def status(self):
        log(self.keys, 'ctx.keys')
        log(self.args, 'ctx.args')


class Console:
    """Небольшой Вспомогательный класс для создания консольного приложения.
    Когда-нибудь будет заменено на argparse..."""

    def __init__(self):
        super(Console, self).__init__()
        self.commands = []

        self.add_command(self.commands_help, [
            'help', 'h', '?'], 'список команд с описанием')
        self.add_command(self.ctx_test, ['ctx'], 'проверить работу Context')

    def add_command(self, func, names, usage, keys={}, use_async=False):
        """Добавляет новую команду (по своей сути именованную функцию)
        :param func: вынкция, для которой создаётся команда
        :param names: список команд, для которых назначается функия
        :param usage: описание, что делает команда
        :optional param keys: передаваемые стандартные ключи для команды
        :optional param use_async: ипользовать ли ассинхронность для команды"""

        self.commands.append(
            {'n': names, 'f': func, 'u': usage, 'a': use_async, 'k': keys})

    def process(self, args):
        """Обрабатывает входящие аргументы.
        Hазбивает на команду, ключи и аргументы.

        :param args: обрабатываемые аргументы
        """

        command = ''
        cargs = []
        keys = {}
        router = {}

        # распределение входных аргументов на ключи и аргументы команды
        for x in args:
            if x[0] == '-':
                x = x.strip('-').split('=', 1)

                if len(x) == 1:
                    keys[x[0]] = True
                elif x[1].isdigit():
                    keys[x[0]] = int(x[1])
                elif x[1].lower() == 'false':
                    keys[x[0]] = False
                else:
                    keys[x[0]] = x[1]

            else:
                cargs.append(x)

        # получаем название команды
        if len(cargs) >= 1:
            command = cargs[0]
            cargs.pop(0)

        # генерируем список комманд
        for v in self.commands:
            for x in v['n']:
                router[x] = v

        if command in router:
            route = router[command]

            for k, v in keys.items():
                route['k'][k] = v

            ctx = Context(cargs, route['k'])

            if route['a']:
                asyncio.run(route['f'](ctx))
            else:
                route['f'](ctx)

        elif command == '':
            self.commands_help(Context())
        else:
            log(f'"{command}" не является командой, "help" для справки', 'w')

    def commands_help(self, ctx):
        """[команда] вывести список всех команд с описанием"""

        print(f'Использование: *.py [keys] [command] [targets]')
        s = max(map(lambda x: len(" ".join(x["n"])), self.commands))

        for v in self.commands:
            keys = []
            for kn, kv in v['k'].items():
                keys.append(f'--{kn}= {kv}')

            if v['u'].count('[') == 1 and v['u'].count(']') == 1:
                x = v['u'][1:].split(']', 1)
                text = x[1]
                for k in x[0].split(';'):
                    keys.append(k.strip())
            else:
                text = v['u']

            if keys:
                keys_str = f"\033[90m{' '.join(keys)}\033[90m"
            else:
                keys_str = ""

            names = " ".join(v["n"])
            names_str = f'\033[94m{names}{" "*(s-len(names))} | '
            print(f'  {names_str}{keys_str}\033[0m {text}')

    def ctx_test(self, ctx):
        """[команда] проверка работы Context"""

        log('test Context class (user input):')
        log(f'keys: {ctx.keys}')
        log(f'arhs: {ctx.args}')
        log(f'get key aaa or "bbb": {ctx.get("aaa", "bbb")}')

        log(f'iteration ctx:')
        for a in ctx:
            log(f'-- {a}')


# классы управлнием пакетами
# ==========================

class Package:
    """Класс пакета"""

    def __init__(self, pid, data={}, config={}):
        super(Package, self).__init__()
        self.pid = pid
        self.config = config
        self.data = check_params(data,
                                 self.config.get('package_model', 
                                 package_base))

        self.__dict__.update(self.data)

    def is_installed(self):
        """Проверяет установлен ли пакет в системе по его файлам."""
        if not self.data['files']:
            log(f'{self.pid} отцуцтвует файлы пакета', 'w')
            return False

        for k in self.data['files']:
            if not os.path.exists(k):
                log(f'отцуцтвует "{k}" --> {self.pid} не установлен', 'w')
                return False

        return True

    def get_file_hash(self, path):
        """Возвращает хеш файла пакета, если он записан."""
        return self.data.get('files', {}).get(path, '')

    def status(self, mini=False, desc=True):
        """Выводит краткую характеристику пакета
        :param mini: показывать ли краткую верcию"""
        res = "  "

        if len(self.pid.split('.')) > 1:
            res += f"\033[95m{self.pid.split('.')[0]} "

        res += f"\033[93m{self.pid.split('.')[-1]}\033[90m"

        if mini:
            res += f'{self.ver}\033[0m '
            if desc:
                res += self.desc
        else:
            res += f' {self.name} \033[92m{self.ver} ({self.build})\033[0m'
            if desc:
                res += f'\n    {self.desc}'

        print(res)

    def print_params(self):
        """Выводит параметры пакета"""
        print_dict(self.data)

    def remove(self):
        """удаляет директорию пакета"""

        if self.path == '.':
            log('пакет находится в корне рабочей директории', 'e')

        elif os.path.isdir(self.path):
            shutil.rmtree(os.path.join(self.path))
            log(f'{self.path} deleted', "c")

        elif not self.path:
            log(f'{self.path}: находится в корневом каталоге', 'w')
        else:
            log(f'{self.path}: пакет не найден', 'e')


class PackagesIndex(object):
    """Управление пакетным индексом"""

    def __init__(self, data, config):
        super(PackagesIndex, self).__init__()
        self.data = data
        self.config = config

    # добавление и обработка данных индекса
    # =====================================

    def extended_info(self):
        """собирает расширенную информацию о пакетах, ищет проблемы пакетв."""
        group('Поиск расширенной информации.')
        used = {}

        for k, v in self.data.items():
            for x in v['require']:
                if isinstance(x, list):
                    x = select(x)

                if x not in self.data:
                    log(f'{k}: не найдена зависимость {x}', 'w')
                    continue

                p = self.get_package(x)
                if not p.is_installed():
                    continue

                if x not in used:
                    used[x] = []

                p.data['used'].append(k)
                used[x].append(k)
                self.data[x] = p.data
        
        for k, v in used.items():
            p = f'\033[93m{k}\033[0m'
            log(f'{p} use \033[32m{len(v)} packages\033[0m')

    def add_package(self, p):
        """Добавляет пакет в индекс.
        :param p: экзмепляр Package"""

        path = p.data["path"].strip('./')

        if not p.data["files"]:
            p.data["files"] = get_package_files(path)
            
        elif isinstance(p.data["files"], list):
            hashes = {}

            if not 'packages.toml' in p.data["files"]:
                p.data["files"].append('packages.toml')

            files = map(lambda x: os.path.join(path, x), p.data["files"])
            for file in files:
                hashes[file] = get_file_hash(file)

            p.data["files"] = hashes

        if not p.is_installed():
            return False

        self.data[p.pid] = p.data
        log(f'{p.pid}: {p.data["path"]}', 'c')

    def get_require(self, args):
        """Получает все зависимости пакетов.
        :param args: список pid, для которых нужно получить зависимости

        :returns: список PID со всеми зависимостями"""
        res = []

        for x in args:
            if isinstance(x, list):
                x = select(x)

            if x in res or x == "*":
                continue

            if x not in self.data:
                log(f'{x}: не улалось получить зависимости', 'e')
                continue

            res.append(x)
            args += self.data[x]["require"]
        return res

    def count_types(self):
        """Считает пакеты по их типам.
        :return: выводит словарь {тип пакета: соличество}"""

        res = {'all': len(self.data)}
        for k, v in self.data.items():
            res[v["type"]] = res.get(v["type"], 0) + 1

        return res

    # поиск локальных пакетов
    # =======================

    def compile(self):
        """Ищет локальные пакеты начиная с работчей директории и добавляет в индекс"""
        res = {}
        places = ['.']
        group('Сборка пакетного индекса.')

        # проходимся по папкам
        for place in dir_iterator(places):
            for x in os.listdir(place):
                path = os.path.join(place, x)

                # добавляем новые папки для поиска
                if os.path.isdir(path):
                    places.append(path)
                    continue

                # если найден нужный файл, добавляем
                if x == 'packages.toml':
                    packages = load_file(path)

                    for k, v in packages.items():
                        if len(packages) > 1:
                            pid = place+"/"+k
                        else:
                            pid = place

                        pid = pid.strip('./')

                        p = Package(pid, v, self.config)
                        p.data['path'] = place
                        p.data['used'] = []
                        self.add_package(p)

        self.extended_info()

    # получение пакетаов из индекса
    # =============================

    def get_package(self, pid, data={}):
        """Возвращает Package экземпляр по данным пакета.
        Если пакет есть в идесе, венёт его данные.
        :param pid: package ID
        :optional param data: атрибуты пакета

        :return: экземпляр Package"""

        if pid in self.data:
            return Package(pid, self.data[pid], self.config)

        elif data:
            return Package(pid, data, self.config)

        return Package('nodata', {}, self.config)

    def get_packages_by_pid(self, names=[]):
        """Более гибкий спосб получения PID пакетов.
        Ищет пакеты по примерным названиям и прдоставляет вам выбор.

        :optional param names: список шаблонов для поиска

        :returm: список PID пакетов"""
        packages = []

        for n in names:
            res = []

            # поиск пакетов по PID с помощью выражений
            for k, v in self.data.items():
                if k.count('.'):
                    pk = k.split('.', 1)[1]
                else:
                    pk = k

                if fnmatch.fnmatch(pk, n) or n in pk:
                    res.append(k)

            # обработка результата
            if len(res) == 1:
                packages.append(res[0])

            elif len(res) and len(res) <= 20:
                selected = select(['pass', 'all packages'] + res)

                if selected == 'pass':
                    continue
                elif selected == "all packages":
                    packages += res

                else:
                    packages.append(selected)

            else:
                log(f'{n}: {len(res)} совпадений: {", ".join(res)}', 'w')

        return packages

    def packages_list(self, packages=[], mini=True, desc=True):
        """Отоюражает статусы сиска пакетов.
        :param packages: список PID пакетов
            если отцуцтвует - все пакеты индекса"""

        if not packages:
            packages = self.data.keys()

        for x in packages:
            self.get_package(x).status(mini, desc)

    def remove_packages(self, packages=[]):
        """Удаляет пакеты из системы.

        :param packages: список PID для удаления"""

        packages = self.get_packages_by_pid(packages)
        self.packages_list(packages)
        if do_you_want_to_continue():
            for x in packages:
                self.get_package(x).remove()


# Главный класс скрипта, управляет индексами пакетов
# ==================================================

class PackagesManager(Console):
    """docstring for PackagesManager"""

    def __init__(self):
        super(PackagesManager, self).__init__()
        self.config = Config(cpm_data_path).read()
        self.rpath = os.path.join(cpm_data_path, 'repositories/')

        # определение команд консольного модуля
        # =====================================

        self.add_command(self.locate_packages, ['locate'],
                         usage='ищет локальные пакеты и генерирует index файл')

        self.add_command(self.remove_packages, ['remove', 'rm'],
                         usage='[targets] удаляет пакеты из системы')

        self.add_command(self.packages_info, ['package', 'p'],
                         usage='[-g targets] показывает информацию о пакете')

        self.add_command(self.packages_status, ['list', 'l'],
                         usage='[-g] показывает статус пакетов',
                         keys={'status': False})

        self.add_command(self.index_info, ['info'],
                         usage='[-g] показывает информацию о настройках и индексах CPM')

        self.add_command(self.search_packages, ['search', 's'],
                         usage='[-g pattern] ищет пакеты по параметрам в индексе')

        self.add_command(self.compile_global_index, ['update', 'u'],
                         usage='[-f] обновляет информацию о глобальном индексе пакетов',
                         use_async=True)

        self.add_command(self.install_packages, ['install', 'i'],
                         usage='[-f targets] устанавливает пакеты из глобального индекса',
                         use_async=True)

        self.add_command(self.get_upgradable_packages, ['upgrade', 'U'],
                         usage='проверяет обновления для пакетов и обновляет их',
                         use_async=True)

    def select_index(self, ctx):
        """Выбирает индекс для тех команд, которые это поддерживают.
        Если в команде указан ключ -g, выдаётся глобальный индекс"""

        if ctx.get('g') or ctx.get('global'):
            return PackagesIndex(load_file('global_'+indexfile), self.config)
        else:
            return PackagesIndex(load_file(indexfile), self.config)

    # команды для локального индекса пакетов
    # ======================================

    def locate_packages(self, ctx):
        """Команда. Ищет локальные пакеты и генерирует индекс пакетов.
        Также проверяет установленные пакеты и ищет битые зависимости.
        """

        p = PackagesIndex({}, self.config)
        p.compile()
        save_file(indexfile, p.data)

    def remove_packages(self, ctx):
        """Удаляет локальные пакетв из системы."""
        group(f'Удаление {ctx.largs}')
        index = PackagesIndex(load_file(indexfile), self.config)
        index.remove_packages(ctx.args)

        self.locate_packages(ctx)

    # команды для смешанного индекса пакетов
    # ======================================

    def packages_info(self, ctx):
        """Отображает информацию о пакетах из индекса."""

        group(f'информацию о пакетах: {ctx.largs}')
        index = self.select_index(ctx)
        for pid in index.get_packages_by_pid(ctx.args):
            index.get_package(pid).print_params()

    def packages_status(self, ctx):
        """Отображает статусы пакетов в индексе."""
        index = self.select_index(ctx)
        if 'no-usage' in ctx.keys:
            filtered = filter(lambda x: x[1]['used'], index.data.items())
            packages = []
            for x in filtered:
                k, _ = x
                packages.append(k)
        else:
            packages = []

        if ctx.get('status'):
            index.packages_list(packages, mini=False, desc=False)
        else:
            index.packages_list(packages)


    def index_info(self, ctx):
        """Отображает информацию о настройках cpm и индексе пакетов."""
        index = self.select_index(ctx)

        group('Настройки по умолчанию для пакетов.')
        print_dict(self.config['package_model'])
        group('Реопзитории.')
        print_dict(self.config['repositories'])

        packages_types = {'all': len(index.data)}
        for k, v in index.data.items():
            packages_types[v['type']] = packages_types.get(v['type'], 0) + 1

        group('Типы пакетов.')
        p_types = ''
        for k, v in sorted(index.count_types().items()):
            p_types += f' {v} \033[34m{k}\033[0m; '

        print(p_types)

    def search_packages(self, ctx):
        """Писк пакетов в индексе по параметрам."""
        group(f'Поиск {ctx.sargs}')
        index = self.select_index(ctx)
        s = SearchEngine(index)
        s.search(ctx.sargs)

    # команды для глобального индекса пакетов
    # =======================================

    async def update_global_index(self, ctx):
        """Загружает пакетные индексы со сторонних репозиторив."""
        group('Обновление реопзиториев.')
        now = int(time.time())
        load = {}
        edited = False

        for k, v in self.config['repositories'].items():
            if v['updated']+(3600*20) < now or ctx.get('f'):
                load[self.rpath+k +
                     '.toml'] = os.path.join(v['url'], indexfile+'.toml')
                v['updated'] = now
                edited = True

            self.config['repositories'][k] = v

        if edited:
            save_file(cpm_data_path+'repositories.toml',
                      self.config['repositories'])

        if not load:
            log(f'все реопзиторие в актуальном состоянии')
            return True

        async with aiohttp.ClientSession() as session:
            await load_files(session, load)

    async def compile_global_index(self, ctx):
        """Собирает глобальный индексный файл из индексных файлов."""
        await self.update_global_index(ctx)
        global_index = {}

        for k, v in self.config['repositories'].items():
            res = load_file(os.path.join(self.rpath, k+'.toml'))
            index = PackagesIndex(res, self.config)

            for p, d in index.data.items():
                d = check_params(d, package_base)
                requirements = []

                for x in d['require']:
                    if isinstance(x, list):
                        res = []
                        for xx in x:
                            if xx.count('.'):
                                res.append(xx)
                            else:
                                res.append(k+'.'+xx)
                        requirements.append(res)
                        continue

                    if x.count('.'):
                        requirements.append(x)
                    else:
                        requirements.append(k+'.'+x)

                d['require'] = requirements
                d['url'] = v['url']
                global_index[k+'.'+p] = d

        save_file('global_'+indexfile, global_index)

    async def load_packages(self, packages, index, gindex):
        """Загружает пакеты по PID из глобального индекса.
        :param packages: список PID лля загрузки
        :param index: локальеый индекс пакетов
        :param gindex: глобальный индекс пакетов"""
        load = {}

        for pid in packages:
            p = index.get_package(pid.split('.', 1)[1])
            gp = gindex.get_package(pid)

            url = gp.data.get('url')

            if not url:
                log(f'{k}: отцуцтвует ссылка для загрузки файлов', 'e')
                continue

            log(f'загрузка {pid} -> {gp.path}..', 'a')

            for k, v in gp.files.items():
                k = k.strip('/')
                name = k.rsplit('/', 1)[-1]
                path = gp.path+"/"+name

                if v == p.get_file_hash(path):
                    log(f'pass {path}', 'c')
                    continue

                load[path] = os.path.join(url, k)

        async with aiohttp.ClientSession() as session:
            await load_files(session, load)

    async def install_packages(self, ctx):
        """загрузить пакеты из репозитория
        :param args: список PID`ов пакетов, можно использовать регулярные выражения"""
        group(f'Установка пакетов {ctx.largs}.')

        # получение паектов и зависимостей
        await self.update_global_index(ctx)
        index = PackagesIndex(load_file(indexfile), self.config)
        gindex = PackagesIndex(load_file('global_'+indexfile), self.config)
        r = gindex.get_require(gindex.get_packages_by_pid(ctx))

        # отсекаем пакеты, совпадающие номером сборки
        for pid in list(r):
            p = index.get_package(pid.split('.', 1)[1])
            gp = gindex.get_package(pid)

            if p.build >= gp.build and gp.build > 1 and not ctx.get('f'):
                log(f'pass {pid}', 'c')
                r.remove(pid)

        if r:
            gindex.packages_list(r)
        else:
            return False

        if do_you_want_to_continue():
            await self.load_packages(r, index, gindex)
            self.locate_packages(ctx)

    async def get_upgradable_packages(self, ctx):
        """Проверяет обновления для локальных пакетов. 
        C последующим обновлением."""
        group('Проверка обновлений.')
        upgradeble = []

        # обновление файлов
        self.locate_packages(ctx)
        await self.update_global_index(ctx)
        index = PackagesIndex(load_file(indexfile), self.config)
        gindex = PackagesIndex(load_file('global_'+indexfile), self.config)

        # проходимся по локальным пакетам и смотрим что можно обновить
        for k, v in index.data.items():
            pid = gindex.get_packages_by_pid([k])

            if not len(pid):
                continue

            pid = pid[0]

            if pid in gindex.data:
                p = gindex.data[pid]

                if v["build"] < p["build"]:
                    log(f'{k} V{v["ver"]} ({v["build"]}) -> V{p["ver"]} ({p["build"]})')
                    upgradeble.append(pid)

                elif v["build"] > p["build"]:
                    log(f'{k} V{v["ver"]} ({v["build"]}) <- {p["ver"]} ({p["build"]})')

        if not upgradeble:
            log(f'все пакеты в актуальном состоянии')
            return True

        if do_you_want_to_continue():
            await self.load_packages(upgradeble, index, gindex)
            self.locate_packages(ctx)


if __name__ == '__main__':
    PackagesManager().process(sys.argv[1:])
